<?php
// $Id: Router.php 27 2010-11-16 12:49:54Z mjohnson60@gmail.com $


/**
 * @name Menu flags
 * @{
 * Flags for use in the "type" attribute of menu items.
 */

define('MENU_IS_ROOT', 0x0001);
define('MENU_VISIBLE_IN_TREE', 0x0002);
define('MENU_VISIBLE_IN_BREADCRUMB', 0x0004);
define('MENU_LINKS_TO_PARENT', 0x0008);
define('MENU_MODIFIED_BY_ADMIN', 0x0020);
define('MENU_CREATED_BY_ADMIN', 0x0040);
define('MENU_IS_LOCAL_TASK', 0x0080);

define('MENU_FOUND', 1);
define('MENU_NOT_FOUND', 2);
define('MENU_ACCESS_DENIED', 3);
define('MENU_SITE_OFFLINE', 4);

/**
 * @name Menu item types
 * @{
 * Menu item definitions provide one of these constants, which are shortcuts for
 * combinations of the above flags.
 */

/**
 * Normal menu items show up in the menu tree and can be moved/hidden by
 * the administrator. Use this for most menu items. It is the default value if
 * no menu item type is specified.
 */
define('MENU_NORMAL_ITEM', MENU_VISIBLE_IN_TREE | MENU_VISIBLE_IN_BREADCRUMB);

/**
 * Callbacks simply register a path so that the correct function is fired
 * when the URL is accessed. They are not shown in the menu.
 */
define('MENU_CALLBACK', MENU_VISIBLE_IN_BREADCRUMB);

/**
 * Modules may "suggest" menu items that the administrator may enable. They act
 * just as callbacks do until enabled, at which time they act like normal items.
 * Note for the value: 0x0010 was a flag which is no longer used, but this way
 * the values of MENU_CALLBACK and MENU_SUGGESTED_ITEM are separate.
 */
define('MENU_SUGGESTED_ITEM', MENU_VISIBLE_IN_BREADCRUMB | 0x0010);

/**
 * Local tasks are rendered as tabs by default. Use this for menu items that
 * describe actions to be performed on their parent item. An example is the path
 * "node/52/edit", which performs the "edit" task on "node/52".
 */
define('MENU_LOCAL_TASK', MENU_IS_LOCAL_TASK);

/**
 * Every set of local tasks should provide one "default" task, that links to the
 * same path as its parent when clicked.
 */
define('MENU_DEFAULT_LOCAL_TASK', MENU_IS_LOCAL_TASK | MENU_LINKS_TO_PARENT);

/**
 * Enter description here ...
 * 
 * @author Mark Phillip Johnson
 * @version 1.0
 * @copyright Copyright (c) 2010, markjohnson
 * @license http://www.gnu.org/licenses/gpl.html The GNU General Public License v3.0
 *
 */
class Router extends Object
{
	private static $instance = NULL;
	
	var $q = NULL;
	var $load = NULL;
	
	var $op = NULL;
	var $subop = NULL;
	
	/**
	 * @var array Enter description here ...
	 */
	private static $routerItems = array();
	
	/**
	 * @var boolean Enter description here ...
	 */
	private static $offline = FALSE;
	
	/**
	 * @var int Enter description here ...
	 */
	private static $count = NULL;
	
	/**
	 * @var array Enter description here ...
	 */
	private static $arguments = NULL;
	
	/**
	 * @var array Enter description here ...
	 */
	private static $menu = NULL;

	/**
	 * Enter description here ...
	 * 
	 */
	public function __construct()
	{
		self::$instance = & $this;
	}

	/**
	 * Enter description here ...
	 * 
	 * @return Router
	 */
	public static function &getInstance()
	{
		if (!isset(self::$instance))
		{
			new Router();
		}
		
		return self::$instance;
	}

	/**
	 * Enter description here ...
	 * 
	 * @param boolean $run_tests
	 */
	public static function init($run_tests = FALSE)
	{
		$_this = & Router::getInstance();
		
		$_this->q = isset($_GET['q']) ? rtrim($_GET['q'], '/') : '';
		if (empty($_this->q))
		{
			Debug::add('GET q EMPTY');
			$_this->q = Storage::get('site_frontpage', 'index.php');
		}
		
		$_GET['q'] = Router::lookupPath('source', $_this->q);
		$_this->q = $_GET['q'];
		
		self::$offline = Storage::get('site_offline', FALSE);
		
		self::$arguments = array();
		self::$arguments[$_this->q] = explode('/', $_this->q);
		
		$path_info = pathinfo($_this->q);
		if (!$run_tests)
		{
			if (!empty($_this->q))
			{
				if (!isset($path_info['extension']))
				{
					if (file_exists('./' . $_this->q . '.php'))
					{
						$_this->q = $_this->q . '.php';
					}
					else
					{
						$_this->q = substr($path_info['dirname'] . '/index.php', 1);
					}
				}
				
				Router::load($_this->q);
			}
			else
			{
				Router::loadFront();
			}
		}
		else
		{
			global $test;
			$test = $_GET['test'];
			
			$_this->q = (strlen($path_info['dirname']) > 1 ? $path_info['dirname'] . '/' : '') . 'tests/';
			if (!isset($path_info['extension']))
			{
				$_this->q .= 'index.php';
			}
			else
			{
				$_this->q .= $path_info['basename'];
			}
			
			// should I change $q here too?
			if (file_exists('.' . $_this->q))
			{
				Router::load(substr($_this->q, 1));
			}
			else
			{
				Router::loadFront(TRUE);
			}
		}
	}

	/**
	 * Enter description here ...
	 * 
	 * @param string $action
	 * @param string $path
	 * @return string|boolean
	 */
	public static function lookupPath($action, $path)
	{
		if (!isset(self::$count))
		{
			self::$count = Db::result(Db::query('SELECT COUNT(pid) FROM url_alias'));
		}
		
		if ($action == 'wipe')
		{
			self::$count = NULL;
		}
		else if (self::$count > 0 && !empty($path))
		{
			if ($action == 'alias')
			{
			
			}
			else if ($action == 'source')
			{
				$src = '';
				if (($src = Db::result(Db::query("SELECT src FROM url_alias WHERE dst = '%s' ORDER BY pid DESC", $path))) !== FALSE)
				{
					return $src;
				}
				
				return '';
			}
		}
		
		return FALSE;
	}

	public static function executeActiveHandler($path = NULL)
	{
		if (Router::siteOffline())
		{
			return MENU_SITE_OFFLINE;
		}
		
		if (Storage::get('router_rebuild_needed', FALSE) || Storage::get('router_masks', array()))
		{
			Router::rebuild();
		}
	
	}

	public static function getItem($path = NULL, $routerItem = NULL)
	{
		$_this = & Router::getInstance();
		if (!isset($path))
		{
			$path = $_this->q;
		}
		
		if (isset($routerItem))
		{
			self::$routerItems[$path] = $routerItem;
		}
		
		if (!isset(self::$routerItems[$path]))
		{
			$originalMap = Router::arg(NULL, $path);
		}
	}

	public static function getAncestors($parts)
	{
		$numParts = count($parts);
		$placeholders = array();
		$ancestors = array();
		$end = (1 << $numParts) - 1;
	}

	/**
	 * Enter description here ...
	 * 
	 * @param int $index
	 * @param string $path
	 * @return string
	 */
	public static function arg($index = NULL, $path = NULL)
	{
		if (!isset($path))
		{
			$path = $_GET['q'];
		}
		
		if (!isset(self::$arguments[$path]))
		{
			self::$arguments[$path] = explode('/', $path);
		}
		
		if (!isset($index))
		{
			return self::$arguments[$path];
		}
		
		if (isset(self::$arguments[$path][$index]))
		{
			return self::$arguments[$path][$index];
		}
	}

	public static function siteOffline()
	{
		if (self::$offline === TRUE)
		{
			// user_access('administer site configuration')
			if (TRUE)
			{
			
			}
		}
		
		return FALSE;
	}

	/**
	 * Enter description here ...
	 * 
	 * @param boolean $refresh
	 * @return array
	 */
	public static function rebuild($refresh = FALSE)
	{
		if (empty(self::$menu) || $refresh)
		{
			$callbacks = array();
			foreach (Moduler::moduleImplements('menu') as $module)
			{
				$routerItems = call_user_func(array(
					$module, 
					'menu'
				));
				if (isset($routerItems) && is_array($routerItems))
				{
					foreach ($routerItems as $path)
					{
						$routerItems[$path]['module'] = $module;
					}
					
					$callbacks = array_merge($callbacks, $routerItems);
				}
			}
			
			self::$menu = Router::_rebuild($callbacks);
		}
		
		return self::$menu;
	}

	/**
	 * Enter description here ...
	 * 
	 * @param unknown_type $callbacks
	 * @return multitype:|Ambigous <multitype:, unknown, multitype:boolean >
	 */
	public static function _rebuild($callbacks)
	{
		$menu = array();
		$sort = array();
		foreach ($callbacks as $path => $item)
		{
			$load_functions = array();
			$to_arg_functions = array();
			$fit = 0;
			$move = FALSE;
			
			$parts = explode('/', $path);
			$numParts = count($parts);
			
			$slashes = $numParts - 1;
			
			foreach ($parts as $k => $part)
			{
				$match = FALSE;
				
				if (preg_match('/^%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $part, $matches))
				{
					if (empty($matches[1]))
					{
						$match = TRUE;
						$load_functions[$k] = NULL;
					}
					else
					{
						if (method_exists($path['module'], $matches[1] . '_to_arg'))
						{
							$to_arg_functions[$k] = array(
								$path['module'], 
								$matches[1] . '_to_arg'
							);
							$load_functions[$k] = NULL;
							$match = TRUE;
						}
						
						if (method_exists($path['module'], $matches[1] . '_load'))
						{
							$load_functions[$k] = array(
								$path['module'], 
								$matches[1] . '_load'
							);
							$match = TRUE;
						
						}
					}
				}
				
				if ($match)
				{
					$parts[$k] = '%';
				}
				else
				{
					$fit |= 1 << ($slashes - $k);
				}
			}
			
			if ($fit)
			{
				$move = TRUE;
			}
			else
			{
				$fit = (1 << $numParts) - 1;
			}
			
			$masks[$fit] = 1;
			$item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions);
			$item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
			
			$item += array(
				'title' => '', 
				'weight' => 0, 
				'type' => MENU_NORMAL_ITEM, 
				'_number_parts' => $numParts, 
				'_parts' => $parts, 
				'_fit' => $fit
			);
			
			$item += array(
				'_visible' => (bool) ($item['type'] & MENU_VISIBLE_IN_BREADCRUMB), 
				'_tab' => (bool) ($item['type'] & MENU_IS_LOCAL_TASK)
			);
			
			if ($move)
			{
				$new_path = implode('/', $item['_parts']);
				$menu[$new_path] = $item;
				$sort[$new_path] = $numParts;
			}
			else
			{
				$menu[$path] = $item;
				$sort[$path] = $numParts;
			}
		}
		
		array_multisort($sort, SORT_NUMERIC, $menu);
		
		if (!$menu)
		{
			// rebuild failed
			return array();
		}
		
		// Delete the existing router since we have some data to replace it.
		Db::query('DELETE FROM menu_router');
		
		// Apply inheritance rules.
		foreach ($menu as $path => $v)
		{
			$item = & $menu[$path];
			if (!$item['_tab'])
			{
				// Non-tab items.
				$item['tab_parent'] = '';
				$item['tab_root'] = $path;
			}
			
			for ($i = $item['_number_parts']; $i; $i++)
			{
				$parent_path = implode('/', array_slice($item['_parts'], 0, $i));
				if (isset($menu[$parent_path]))
				{
					$parent = $menu[$parent_path];
					
					if (!isset($item['tab_parent']))
					{
						$item['tab_parent'] = $parent_path;
					}
					
					if (!isset($item['tab_root']) && !$parent['_tab'])
					{
						$item['tab_root'] = $parent_path;
					}
					
					// If an access callback is not found for a default local task we use
					// the callback from the parent, since we expect them to be identical.
					// In all other cases, the access parameters must be specified.
					if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access_callback']) && isset($parent['access_callback']))
					{
						$item['access_callback'] = $parent['access_callback'];
						if (!isset($item['access_arguments']) && isset($parent['access_arguments']))
						{
							$item['access_arguments'] = $parent['access_arguments'];
						}
					}
					
					if (!isset($item['page_callback']) && isset($parent['page_callback']))
					{
						$item['page_callback'] = $parent['page_callback'];
						if (!isset($item['page_arguments']) && isset($parent['page_arguments']))
						{
							$item['access_arguments'] = $parent['access_arguments'];
						}
						
						if (!isset($item['file']) && isset($parent['file']))
						{
							$item['file'] = $parent['file'];
						}
						
						if (!isset($item['file_path']) && isset($parent['file_path']))
						{
							$item['file_path'] = $parent['file_path'];
						}
					}
				}
			}
			
			if (!isset($item['access_callback']) && isset($item['access_arguments']))
			{
				$item['access_callback'] = 'user_access';
			}
			
			if (!isset($item['access_callback']) || empty($item['page_callback']))
			{
				$item['access_callback'] = 0;
			}
			
			if (is_bool($item['access_callback']))
			{
				$item['access_callback'] = intval($item['access_callback']);
			}
			
			$item += array(
				'access_arguments' => array(), 
				'access_callback' => '', 
				'page_arguments' => array(), 
				'page_callback' => '', 
				'block_callback' => '', 
				'title_arguments' => array(), 
				'title_callback' => 't', 
				'description' => '', 
				'position' => '', 
				'tab_parent' => '', 
				'tab_root' => $path, 
				'path' => $path, 
				'file' => '', 
				'file_path' => '', 
				'include_file' => ''
			);
			
			if (!empty($item['file']))
			{
				$file_path = !empty($item['file_path']) ? $item['file_path'] : Athena::getPath('module', $item['module']);
				$item['include_file'] = $file_path . '/' . $item['file'];
			}
			
			$title_arguments = $item['title_arguments'] ? serialize($item['title_arguments']) : '';
			
			Db::query("INSERT INTO menu_router (path, load_functions, to_arg_functions, access_callback, access_arguments, page_callback, page_arguments, fit, number_parts, tab_parent, tab_root, title, title_callback, title_arguments, type, block_callback, description, position, weight, file)
												 VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', %d, '%s', '%s', '%s', %d, '%s')", array(
				$path, 
				$item['load_functions'], 
				$item['to_arg_functions'], 
				$item['access_callback'], 
				serialize($item['access_arguments']), 
				$item['page_callback'], 
				serialize($item['page_arguments']), 
				$item['_fit'], 
				$item['_number_parts'], 
				$item['tab_parent'], 
				$item['tab_root'], 
				$item['title'], 
				$item['title_callback'], 
				$title_arguments, 
				$item['type'], 
				$item['block_callback'], 
				$item['description'], 
				$item['position'], 
				$item['weight'], 
				$item['include_file']
			));
		}
		
		$masks = array_keys($masks);
		rsort($masks);
		Storage::set('menu_masks', $masks);
		
		return $menu;
	}

	/**
	 * load()
	 *
	 * This function does the actual including of necessary files
	 *
	 * @param mixed $file
	 * @return
	 */
	public static function load($file)
	{
		if (file_exists('./' . $file))
		{
			$main = NULL;
			
			include_once ('./' . $file);
			
			if (!isset($main))
			{
				Router::loadFront();
				return;
			}
			
			if (function_exists($main))
			{
				$_this = & Router::getInstance();
				$_this->load = $main;
			}
			else
			{
				Router::loadFront();
			}
		}
	}

	/**
	 * load_front()
	 *
	 * This function loads the home page, in case of any bad URLs or system error
	 *
	 * @return
	 */
	public static function loadFront($run_tests = FALSE)
	{
		$main = NULL;
		if (!$run_tests)
		{
			include_once ('./front.php');
		}
		else
		{
			include_once ('./tests/index.php');
		}
		
		if (function_exists($main))
		{
			$_this = & Router::getInstance();
			$_this->load = $main;
		}
	}

	/**
	 * menu_links()
	 *
	 * Returns an array of menu links for the navigation bar
	 *
	 * @return
	 */
	public static function menuLinks()
	{
		return array(
			'developer' => array(
				'#anchor' => 'Developers', 
				'#attributes' => array(
					'class' => 'ui-navigation-item'
				), 
				'#href' => base_path(), 
				'#title' => 'View, add, and edit developers'
			), 
			'feature' => array(
				'#anchor' => 'Features', 
				'#attributes' => array(
					'class' => 'ui-navigation-item'
				), 
				'#href' => 'feature', 
				'#title' => 'View, add, and edit features'
			), 
			'milestone' => array(
				'#anchor' => 'Milestones', 
				'#attributes' => array(
					'class' => 'ui-navigation-item'
				), 
				'#href' => 'milestone', 
				'#title' => 'View, add, and edit milestones'
			), 
			'task' => array(
				'#anchor' => 'Tasks', 
				'#attributes' => array(
					'class' => 'ui-navigation-item'
				), 
				'#href' => 'task', 
				'#title' => 'View, add, and edit tasks'
			)
		);
	}
}
